/* eslint-disable no-labels */
/* eslint-disable padded-blocks */
/* eslint-disable one-var */
/* eslint-disable semi */
/* eslint-disable wrap-iife */
import * as THREE from 'three'

export const SkeletonUtils = {

  retarget: function () {

    var pos = new THREE.Vector3(),
      quat = new THREE.Quaternion(),
      scale = new THREE.Vector3(),
      bindBoneMatrix = new THREE.Matrix4(),
      relativeMatrix = new THREE.Matrix4(),
      globalMatrix = new THREE.Matrix4();

    return function (target, source, options) {

      options = options || {};
      options.preserveMatrix = options.preserveMatrix !== undefined ? options.preserveMatrix : true;
      options.preservePosition = options.preservePosition !== undefined ? options.preservePosition : true;
      options.preserveHipPosition = options.preserveHipPosition !== undefined ? options.preserveHipPosition : false;
      options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false;
      options.hip = options.hip !== undefined ? options.hip : 'hip';
      options.names = options.names || {};

      var sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones(source),
        bones = target.isObject3D ? target.skeleton.bones : this.getBones(target),
        bindBones,
        bone, name, boneTo,
        bonesPosition, i;

      // reset bones

      if (target.isObject3D) {

        target.skeleton.pose();

      } else {

        options.useTargetMatrix = true;
        options.preserveMatrix = false;

      }

      if (options.preservePosition) {

        bonesPosition = [];

        for (i = 0; i < bones.length; i++) {

          bonesPosition.push(bones[i].position.clone());

        }

      }

      if (options.preserveMatrix) {

        // reset matrix

        target.updateMatrixWorld();

        target.matrixWorld.identity();

        // reset children matrix

        for (i = 0; i < target.children.length; ++i) {

          target.children[i].updateMatrixWorld(true);

        }

      }

      if (options.offsets) {

        bindBones = [];

        for (i = 0; i < bones.length; ++i) {

          bone = bones[i];
          name = options.names[bone.name] || bone.name;

          if (options.offsets && options.offsets[name]) {

            bone.matrix.multiply(options.offsets[name]);

            bone.matrix.decompose(bone.position, bone.quaternion, bone.scale);

            bone.updateMatrixWorld();

          }

          bindBones.push(bone.matrixWorld.clone());

        }

      }

      for (i = 0; i < bones.length; ++i) {

        bone = bones[i];
        name = options.names[bone.name] || bone.name;

        boneTo = this.getBoneByName(name, sourceBones);

        globalMatrix.copy(bone.matrixWorld);

        if (boneTo) {

          boneTo.updateMatrixWorld();

          if (options.useTargetMatrix) {

            relativeMatrix.copy(boneTo.matrixWorld);

          } else {

            relativeMatrix.getInverse(target.matrixWorld);
            relativeMatrix.multiply(boneTo.matrixWorld);

          }

          // ignore scale to extract rotation

          scale.setFromMatrixScale(relativeMatrix);
          relativeMatrix.scale(scale.set(1 / scale.x, 1 / scale.y, 1 / scale.z));

          // apply to global matrix

          globalMatrix.makeRotationFromQuaternion(quat.setFromRotationMatrix(relativeMatrix));

          if (target.isObject3D) {

            var boneIndex = bones.indexOf(bone),
              wBindMatrix = bindBones ? bindBones[boneIndex] : bindBoneMatrix.getInverse(target.skeleton.boneInverses[boneIndex]);

            globalMatrix.multiply(wBindMatrix);

          }

          globalMatrix.copyPosition(relativeMatrix);

        }

        if (bone.parent && bone.parent.isBone) {

          bone.matrix.getInverse(bone.parent.matrixWorld);
          bone.matrix.multiply(globalMatrix);

        } else {

          bone.matrix.copy(globalMatrix);

        }

        if (options.preserveHipPosition && name === options.hip) {

          bone.matrix.setPosition(pos.set(0, bone.position.y, 0));

        }

        bone.matrix.decompose(bone.position, bone.quaternion, bone.scale);

        bone.updateMatrixWorld();

      }

      if (options.preservePosition) {

        for (i = 0; i < bones.length; ++i) {

          bone = bones[i];
          name = options.names[bone.name] || bone.name;

          if (name !== options.hip) {

            bone.position.copy(bonesPosition[i]);

          }

        }

      }

      if (options.preserveMatrix) {

        // restore matrix

        target.updateMatrixWorld(true);

      }

    };

  }(),

  retargetClip: function (target, source, clip, options) {

    options = options || {};
    options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false;
    options.fps = options.fps !== undefined ? options.fps : 30;
    options.names = options.names || [];

    if (!source.isObject3D) {

      source = this.getHelperFromSkeleton(source);

    }

    var numFrames = Math.round(clip.duration * (options.fps / 1000) * 1000),
      delta = 1 / options.fps,
      convertedTracks = [],
      mixer = new THREE.AnimationMixer(source),
      bones = this.getBones(target.skeleton),
      boneDatas = [],
      positionOffset,
      bone, boneTo, boneData,
      name, i, j;

    mixer.clipAction(clip).play();
    mixer.update(0);

    source.updateMatrixWorld();

    for (i = 0; i < numFrames; ++i) {

      var time = i * delta;

      this.retarget(target, source, options);

      for (j = 0; j < bones.length; ++j) {

        name = options.names[bones[j].name] || bones[j].name;

        boneTo = this.getBoneByName(name, source.skeleton);

        if (boneTo) {

          bone = bones[j];
          boneData = boneDatas[j] = boneDatas[j] || { bone: bone };

          if (options.hip === name) {

            if (!boneData.pos) {

              boneData.pos = {
                times: new Float32Array(numFrames),
                values: new Float32Array(numFrames * 3)
              };

            }

            if (options.useFirstFramePosition) {

              if (i === 0) {

                positionOffset = bone.position.clone();

              }

              bone.position.sub(positionOffset);

            }

            boneData.pos.times[i] = time;

            bone.position.toArray(boneData.pos.values, i * 3);

          }

          if (!boneData.quat) {

            boneData.quat = {
              times: new Float32Array(numFrames),
              values: new Float32Array(numFrames * 4)
            };

          }

          boneData.quat.times[i] = time;

          bone.quaternion.toArray(boneData.quat.values, i * 4);

        }

      }

      mixer.update(delta);

      source.updateMatrixWorld();

    }

    for (i = 0; i < boneDatas.length; ++i) {

      boneData = boneDatas[i];

      if (boneData) {

        if (boneData.pos) {

          convertedTracks.push(new THREE.VectorKeyframeTrack(
            '.bones[' + boneData.bone.name + '].position',
            boneData.pos.times,
            boneData.pos.values
          ));

        }

        convertedTracks.push(new THREE.QuaternionKeyframeTrack(
          '.bones[' + boneData.bone.name + '].quaternion',
          boneData.quat.times,
          boneData.quat.values
        ));

      }

    }

    mixer.uncacheAction(clip);

    return new THREE.AnimationClip(clip.name, -1, convertedTracks);

  },

  getHelperFromSkeleton: function (skeleton) {

    var source = new THREE.SkeletonHelper(skeleton.bones[0]);
    source.skeleton = skeleton;

    return source;

  },

  getSkeletonOffsets: function () {

    var targetParentPos = new THREE.Vector3(),
      targetPos = new THREE.Vector3(),
      sourceParentPos = new THREE.Vector3(),
      sourcePos = new THREE.Vector3(),
      targetDir = new THREE.Vector2(),
      sourceDir = new THREE.Vector2();

    return function (target, source, options) {

      options = options || {};
      options.hip = options.hip !== undefined ? options.hip : 'hip';
      options.names = options.names || {};

      if (!source.isObject3D) {

        source = this.getHelperFromSkeleton(source);

      }

      var nameKeys = Object.keys(options.names),
        nameValues = Object.values(options.names),
        sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones(source),
        bones = target.isObject3D ? target.skeleton.bones : this.getBones(target),
        offsets = [],
        bone, boneTo,
        name, i;

      target.skeleton.pose();

      for (i = 0; i < bones.length; ++i) {

        bone = bones[i];
        name = options.names[bone.name] || bone.name;

        boneTo = this.getBoneByName(name, sourceBones);

        if (boneTo && name !== options.hip) {

          var boneParent = this.getNearestBone(bone.parent, nameKeys),
            boneToParent = this.getNearestBone(boneTo.parent, nameValues);

          boneParent.updateMatrixWorld();
          boneToParent.updateMatrixWorld();

          targetParentPos.setFromMatrixPosition(boneParent.matrixWorld);
          targetPos.setFromMatrixPosition(bone.matrixWorld);

          sourceParentPos.setFromMatrixPosition(boneToParent.matrixWorld);
          sourcePos.setFromMatrixPosition(boneTo.matrixWorld);

          targetDir.subVectors(
            new THREE.Vector2(targetPos.x, targetPos.y),
            new THREE.Vector2(targetParentPos.x, targetParentPos.y)
          ).normalize();

          sourceDir.subVectors(
            new THREE.Vector2(sourcePos.x, sourcePos.y),
            new THREE.Vector2(sourceParentPos.x, sourceParentPos.y)
          ).normalize();

          var laterialAngle = targetDir.angle() - sourceDir.angle();

          var offset = new THREE.Matrix4().makeRotationFromEuler(
            new THREE.Euler(
              0,
              0,
              laterialAngle
            )
          );

          bone.matrix.multiply(offset);

          bone.matrix.decompose(bone.position, bone.quaternion, bone.scale);

          bone.updateMatrixWorld();

          offsets[name] = offset;

        }

      }

      return offsets;

    };

  }(),

  renameBones: function (skeleton, names) {

    var bones = this.getBones(skeleton);

    for (var i = 0; i < bones.length; ++i) {

      var bone = bones[i];

      if (names[bone.name]) {

        bone.name = names[bone.name];

      }

    }

    return this;

  },

  getBones: function (skeleton) {

    return Array.isArray(skeleton) ? skeleton : skeleton.bones;

  },

  getBoneByName: function (name, skeleton) {

    for (var i = 0, bones = this.getBones(skeleton); i < bones.length; i++) {

      if (name === bones[i].name) { return bones[i]; }

    }

  },

  getNearestBone: function (bone, names) {

    while (bone.isBone) {

      if (names.indexOf(bone.name) !== -1) {

        return bone;

      }

      bone = bone.parent;

    }

  },

  findBoneTrackData: function (name, tracks) {

    var regexp = /\[(.*)\]\.(.*)/,
      result = { name: name };

    for (var i = 0; i < tracks.length; ++i) {

      // 1 is track name
      // 2 is track type
      var trackData = regexp.exec(tracks[i].name);

      if (trackData && name === trackData[1]) {

        result[trackData[2]] = i;

      }

    }

    return result;

  },

  getEqualsBonesNames: function (skeleton, targetSkeleton) {

    var sourceBones = this.getBones(skeleton),
      targetBones = this.getBones(targetSkeleton),
      bones = [];

    search: for (var i = 0; i < sourceBones.length; i++) {

      var boneName = sourceBones[i].name;

      for (var j = 0; j < targetBones.length; j++) {

        if (boneName === targetBones[j].name) {

          bones.push(boneName);

          continue search;

        }

      }

    }

    return bones;

  },

  clone: function (source) {

    var sourceLookup = new Map();
    var cloneLookup = new Map();

    var clone = source.clone();

    parallelTraverse(source, clone, function (sourceNode, clonedNode) {

      sourceLookup.set(clonedNode, sourceNode);
      cloneLookup.set(sourceNode, clonedNode);

    });

    clone.traverse(function (node) {

      if (!node.isSkinnedMesh) return;

      var clonedMesh = node;
      var sourceMesh = sourceLookup.get(node);
      var sourceBones = sourceMesh.skeleton.bones;

      clonedMesh.skeleton = sourceMesh.skeleton.clone();
      clonedMesh.bindMatrix.copy(sourceMesh.bindMatrix);

      clonedMesh.skeleton.bones = sourceBones.map(function (bone) {

        return cloneLookup.get(bone);

      });

      clonedMesh.bind(clonedMesh.skeleton, clonedMesh.bindMatrix);

    });

    return clone;

  }

};

function parallelTraverse(a, b, callback) {

  callback(a, b);

  for (var i = 0; i < a.children.length; i++) {

    parallelTraverse(a.children[i], b.children[i], callback);

  }
}
